DiffUtil 是 RecyclerView 中自带的一个计算数据集差异性的组件,其内部还提供了对 RecyclerView 进行局部刷新的 API。以往我们对 RecyclerView 的刷新,大多会采用 notifyDataSetChanged,而这种刷新方式,会造成 RecyclerView#Adapter 调用 List#size 次的 onCreateViewHolder 和 onBindViewHolder,很容易就造成资源浪费,在使用了 DiffUtil 之后,无论是添加、删除、还是更新元素,也只会调用对应数量个 onCreateViewHolder 和 onBindViewHolder,从而对 RecyclerView 的使用进行优化。
下面我们就通过一个简单的例子来认识一下这个 DiffUtil。
首先,我们创建出一个简单的列表。然后为其设置 Adapter、LayoutManager 等 RecyclerView 基础配置。
代码如下:
1 | private val numberList = mutableListOf<SimpleText>() |
SimpleAdapter 和 布局文件内容比较简单,就不展示了,仅仅是把 SimpleText 的中的 title 显示到 RecyclerView 的 Item 上。
在没有使用 DiffUtil 时,更新页面上的内容我们需要更新数据集合「List」,然后再次调用 Adapter#notifyDataSetChanged 或 Adapter 的其它局部更新的方法进行刷新即可。现在我们既然要使用 DiffUtill,那么具体该怎么做呢?
首先,我们需要创建一个 DiffUtil.Callback 的实现类,DiffUtil.Callback 是一个抽象类,其中包含了 4 个抽象方法,都是用于比较两个数据集时所必须用到的方法。
1 | class DiffCallback(private val oldList: List<SimpleText>, private val newList: List<SimpleText>) : DiffUtil.Callback() { |
DiffUtil.Callback 需要两个列表,就把新旧两个数据集合在构造函数中传入即可。
到这里,我们猜一下,这个 DiffUtil 的工作原理:先进行判断新旧两个数据集的尺寸是否相同,然后再比较新旧两个数据集同一个下标的所对应的元素是否相同,最后再对元素内容是否一致进行比较。
不管猜的对不对,我们先继续。
在创建完 DiffUtil.Callback 的实现类后,需要对它进行实例化,注意看,DiffCallback 的主构造器声明了两个字段,所以我们在对它进行实例化时也需要传入两个元素。
现在我们有个需求:对数据集合中倒数第二个元素进行编辑。我们该怎么做?按照之前不使用 Diff 的时候,我们应该这么做:
1 | numberList[numberList.size - 2].title = "New Edit To" |
现在呢?自然是用 DiffUtil.Callback 来进行,不过在使用 Diff 之前,我们还需要准备一个 List。因为我们刚才给 DiffCallback 的主构造器声明了两个字段,一个是 oldList,一个是 newList。oldList 就是我们的 numberList,我们还需要准备一个新的 List。
1 | val tmpList = MutableList<SimpleText>(10) { |
数据准备完成后,接下来就是应用在 Diff 上了。
1 | val diffResult = DiffUtil.calculateDiff(DiffCallback(numberList, tmpList)) |
可以看到,就这样很简单的把两个 List 放在 DiffUtil#calculateDiff 中进行计算,然后得出 diffResult,最后将 diffResult 关联到 Adapter 即可。
怎么样?是不是挺简单的?去试一下,向列表添加元素和删除元素吧。
添加和删除元素本质上和编辑元素没太多区别,就不进行阐述了,在提供的代码中都有。
Diff 用起来好像挺简单的,不过 Google 还向我们提供了一个工具,能让我们用起来更方便,这个就是 ListAdapter。
ListAdapter 继承了 RecyclerView#Adapter,并且开发者不需要再实现 DiffCallback 的四个方法,只需要实现其中的两个「areItemsTheSame,areContentsTheSame」即可。也不需要调用拿到 DiffUtil#calculateDiff 的结果了。只需要在需要更新列表的时候,调用 simpleListAdapter.submitList(List)
即可,不过 RecyclerView 的 Adapter 就不能继承 RecyclerView#Adaper 了,而是需要继承自 ListAdapter。
1 | class SimpleDiffItemCallback : DiffUtil.ItemCallback<MainActivity.SimpleText>() { |
仔细看的朋友可能会发现,我在使用 ListAdapter 时传入一个 SimpleDiffItemCallback 这个对象,这个对象实现的抽象类是 DiffUtil.ItemCallback。这个 ItemCallback 和之前使用 Callback 有 3 处不同。
怎么样?ListAdapter 是不是挺简单的?不过我现在有点好奇,ListAdpater 内部具体做了哪些事情?来让我们看看 ListAdapter 的源码。
当我点开 ListAdapter 的源码时,我发现,它内部好像啥也没干,又好像啥都干了。具体来说就是把全部的脏活累活都丢给了 AsyncListDiffer 这个类,ListAdapter 自己就像是一个代理,需要做什么,就丢给 AsyncListDiffer 这个类,需要获取什么就从 AsyncListDiffer 这个类里取出来。
看到这,我继续进入 AsyncListDiffer,发现这个类其实也没特别多的东西,最重要的就是其中两个方法。一个是 submitList,另一个是 latchList。
submitList 其实就是我们在 Activity/Fragment 调用 adapter#submit(List) 后通过各种调用最终的目的地。不过在 submitList 的内部实际上是通过在子线程计算两个列表的差异,并在主线程上通过调用 latchList 将数据关联到 RecyclerView#Adapter 的效果。
latchList 做的内容就很简单了,只是将数据进行绑定。
在这个例子中,我使用的是单一类型的数据集,但是在实际项目中存在多类型数据集该怎么做呢?我这里提供一下思路和简单的示例代码。
思路:
怎么样?Diff 用起来很简单吧?要不要去试一下?而且,偷偷告诉你,Google 发布的另一个库 Paging 内部也有 Diff 的身影哦。
本文示例代码已经上传到了 GitHub,分支为 diff,多类型示例。